Skip to content

feat: lua script commands triggers - #447

Merged
Malaydewangan09 merged 2 commits into
kestra-io:mainfrom
Abhishek84313:feat/lua-script-commands-triggers
Oct 1, 2026
Merged

Malaydewangan09 merged 2 commits into
kestra-io:mainfrom
Abhishek84313:feat/lua-script-commands-triggers

Conversation

@Abhishek84313

@Abhishek84313 Abhishek84313 commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

What changes are being made and why?

Adds ScriptTrigger and CommandsTrigger to plugin-script-lua, which was
one of the submodules still missing them.

Part of #313, closes #428.

Both triggers live in io.kestra.plugin.scripts.lua — the same package as the
Script and Commands tasks, with no nested subpackage — matching the
existing implementations in plugin-script-r, plugin-script-perl,
plugin-script-go, plugin-script-node, plugin-script-python,
plugin-script-ruby and plugin-script-shell.

ScriptTrigger polls by running an inline Lua script through the Script
task and emits an execution when the condition matches. CommandsTrigger
does the same for a list of commands through the Commands task.

Each trigger exposes:

Property Default Description
containerImage nickblah/lua Image used to run the script/commands
script / commands — Required; same semantics as the corresponding task
exitCondition — Required; decides whether the trigger fires
interval PT60S Polling interval
edge true Emit only on a not-matching → matching transition

exitCondition accepts either exit N, which compares against the process
exit code, or any other string, which is treated as a regex (falling back to a
substring match if the regex is invalid) against vars emitted via the
::{"outputs":{...}}:: convention. A blank exitCondition is rejected rather
than silently matching everything.

When edge is enabled the previous result is kept in the namespace KV store,
keyed by flow and trigger id, rather than in an instance field — a polling
trigger is rebuilt from the flow definition and serialized to a worker on every
poll, so in-memory state would not survive between evaluations.

The trigger output carries timestamp, condition, exitCode and vars;
execution id, namespace and flow id come from the standard
TriggerService.generateExecution context. A failed evaluation is logged and
returns empty rather than propagating, so a broken script cannot block the
scheduler.

The implementation follows the reviewed plugin-script-r triggers.

How the changes have been QAed?

Unit and condition-matching tests were added for both triggers
(ScriptTriggerTest, CommandsTriggerTest, ScriptTriggerConditionTest,
CommandsTriggerConditionTest, EdgeStateTest), covering exit-code
conditions, regex and substring matching against emitted vars, empty/null
conditions, edge-mode transition behaviour across fresh trigger instances, and
KV key scoping between flows and triggers.

41 tests that do not require Docker pass locally, and compileJava and
compileTestJava both succeed. The Docker-backed cases in
CommandsTriggerTest could not be run on the development machine, which has no
Docker available — they need a CI run to confirm. They use
lua -e 'os.exit(1)' / os.exit(0) against nickblah/lua:latest, consistent
with the existing CommandsTest and the all_lua.yaml sanity check that
already invoke lua in that image.

Trigger on a failing script:

id: lua_script_trigger
namespace: company.team

triggers:
  - id: script_failure
    type: io.kestra.plugin.scripts.lua.ScriptTrigger
    interval: PT10S
    exitCondition: "exit 1"
    edge: true
    containerImage: nickblah/lua
    script: |
      -- This fails with a non-zero exit code.
      os.exit(1)

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"

@github-project-automation github-project-automation Bot moved this to To review in Pull Requests Sep 20, 2026
@MilosPaunovic MilosPaunovic added kind/external Pull requests raised by community contributors area/plugin Plugin-related issue or feature request labels Sep 21, 2026
@MilosPaunovic
MilosPaunovic requested review from a team and Malaydewangan09 September 21, 2026 06:04
@Malaydewangan09

Copy link
Copy Markdown
Member

This PR doesn't contain any Lua.

The branch is feat/lua-script-commands-triggers and the description walks through the Lua triggers, the properties table, and 36 passing tests, but the diff is:

AGENTS.md
plugin-script-perl/...  (7 files)
plugin-script-r/...     (7 files)

Zero files under plugin-script-lua. The two commits on the branch are feat/r-script-commands-triggers and feat/perl-script-commands-triggers. plugin-script-lua on main still has only Script.java, Commands.java and package-info.java, so the work genuinely isn't here.

Separately, this diff is byte-identical to #446 (both 1950 lines), and #438 is the R subset of both. So #438, #446 and #447 are three PRs carrying overlapping copies of the same commits rather than one module each. Merging any one of them will conflict the other two.

Could you rebase each onto current main with only its own module: #438 R, #446 Perl, and this one Lua? I'll review them properly once they're separated. Nothing here reflects on the code itself, I just can't review Lua triggers that aren't in the diff.

@Malaydewangan09 Malaydewangan09 changed the title Feat/lua script commands triggers feat: lua script commands triggers Sep 22, 2026
@Malaydewangan09

Copy link
Copy Markdown
Member

Hey @Abhishek84313 👋, are there any updates on this?

@Abhishek84313

Copy link
Copy Markdown
Contributor Author

Hey @Abhishek84313 👋, are there any updates on this?

Haven't checked it, i will let you know

Adds ScriptTrigger and CommandsTrigger to plugin-script-lua, one of the
submodules still missing them. Both live in io.kestra.plugin.scripts.lua,
alongside the existing Script and Commands tasks.

ScriptTrigger polls by running an inline Lua script through the Script
task; CommandsTrigger does the same for a list of commands through the
Commands task. Each emits an execution when exitCondition matches, which
accepts either "exit N" (compared against the process exit code) or a
regex, falling back to a substring match, against vars emitted via the
::{"outputs":{...}}:: convention.

Edge state is kept in the namespace KV store rather than in an instance
field, since a polling trigger is rebuilt from the flow definition and
serialized to a worker on every poll. The implementation follows the
reviewed plugin-script-r triggers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Abhishek84313
Abhishek84313 force-pushed the feat/lua-script-commands-triggers branch from 61ad5d1 to 714d81d Compare September 29, 2026 13:51
@Abhishek84313

Copy link
Copy Markdown
Contributor Author

Thanks for catching this @Malaydewangan09, you were right on all counts.

The cause was a one-character branch name mix-up on my side: the actual Lua
work was sitting on my local feat/lua-script-commands-trigger (singular),
while the branch I pushed was feat/lua-script-commands-triggers (plural),
which only ever had the R and Perl commits. Hence the identical diffs across
#438/#446/#447.

I've rebased this branch onto current main. It is now a single commit
touching only plugin-script-lua:

  • ScriptTrigger.java, CommandsTrigger.java
  • ScriptTriggerTest, CommandsTriggerTest, ScriptTriggerConditionTest,
    CommandsTriggerConditionTest, EdgeStateTest

No AGENTS.md, no R, no Perl, and no build.gradle change. Merges cleanly.

Since R (#438) and Perl (#446) are already merged, those two are resolved and
this is the only one left from the set.

While rebasing I noticed my Lua code predated the review feedback on #438, so
I ported both triggers from the merged R implementation rather than ship the
older shape. Three things came across:

  • Edge state moved to the namespace KV store. It was previously tracked in
    an AtomicBoolean field, which does not survive a poll: the trigger is
    rebuilt from the flow definition and serialized to a worker each time, so
    edge: true would have fired on every matching poll instead of only on the
    transition. EdgeStateTest now covers this across fresh instances.
  • Bounded LRU cache for compiled condition patterns, instead of
    recompiling the regex on every poll.
  • Blank exitCondition now throws rather than defaulting to "", which
    previously matched everything.

compileJava and compileTestJava pass, and 41/41 non-Docker tests pass
locally. The Docker-backed cases in CommandsTriggerTest need CI, as I have
no Docker on this machine.

On the issue's proposed commandPattern / argumentsPattern: this PR still
implements only exitCondition, matching every existing reference
implementation. These triggers run the script themselves rather than observing
externally-launched executions, so there is no command stream to match
against. If those properties are wanted, they should land across all submodules
in a separate change.

@Malaydewangan09

Copy link
Copy Markdown
Member

Thanks, this is the right diff now. Only plugin-script-lua, and both triggers are a clean port of the merged R ones (24 lines differ, all names, image and examples).

Ran it on current main with Docker: 49/49 pass, including the container-backed CommandsTriggerTest cases. Also ran the edge cycle through evaluate() against real containers, trigger reserialized every poll:

cmds : exit 1  exit 1  exit 0  exit 0  exit 1
got  : FIRED   quiet   quiet   quiet   FIRED

Two things missing that the R and Perl PRs did include:

  • AGENTS.md still lists only Commands and Script under plugin-script-lua. Please add CommandsTrigger and ScriptTrigger.
  • src/main/resources/doc/io.kestra.plugin.scripts.lua.md has no ## Triggers section. The R doc has one, same shape works here.

One optional: shouldEmit writes the KV entry on every poll even when the result hasn't changed. The PowerShell trigger (#436) only writes when matched != previouslyMatched, which cuts a steady condition from ~1440 writes a day to one per transition. Fine to leave if you'd rather stay identical to R.

Happy to approve once the two docs bits are in.

@Malaydewangan09

Copy link
Copy Markdown
Member

Two more from a closer pass, both small:

  • ScriptTrigger.evaluate() is never run against a real container in this module. ScriptTriggerTest and ScriptTriggerConditionTest only call matchesCondition, and EdgeStateTest calls shouldEmit directly. Only CommandsTriggerTest goes through evaluate(), so the Script task wiring (lua <file> in nickblah/lua, TriggerRunContext) has no coverage. A ScriptTriggerEvaluateTest like the PowerShell one would cover it: os.exit(1) with exitCondition: exit 1 asserting exitCode 1, plus one print('::{"outputs":{"k":"v"}}::') case for the vars path.
  • The exitCondition docs still say the regex is matched against failure logs (ScriptTrigger.java:108 "emitted vars and failure logs", CommandsTrigger.java:112 "vars + logs"). Logs are never read. buildHaystack is only vars.toString(), and a failed run has no vars, so only exit N can match a failure. The PowerShell wording is accurate: "matched against emitted vars; a failed run has no vars, so only 'exit N' can match a failure."

- List CommandsTrigger and ScriptTrigger under plugin-script-lua in AGENTS.md
- Add a Triggers section to the Lua plugin doc
- Fix exitCondition docs: the regex is matched against emitted vars only,
  logs are never read, so only 'exit N' can match a failed run
- Add ScriptTriggerEvaluateTest driving ScriptTrigger.evaluate() against a
  real nickblah/lua container (exit code, emitted vars, no match, edge mode)
- Only write the edge state to KV when the result changes, as in the
  PowerShell triggers, with matching EdgeStateTest coverage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Abhishek84313

Copy link
Copy Markdown
Contributor Author

Thanks @Malaydewangan09, all addressed in 8521610:

AGENTS.md: added CommandsTrigger and ScriptTrigger under plugin-script-lua.
Docs: added a ## Triggers section to io.kestra.plugin.scripts.lua.md, same shape as R.
exitCondition wording: fixed in both triggers. It no longer mentions logs, and now says the regex matches emitted vars only, so only exit N can match a failure (PowerShell wording).
ScriptTriggerEvaluateTest: runs evaluate() against nickblah/lua for os.exit(1) + exit 1 (expects exitCode 1), print('::{"outputs":{"k":"v"}}::') (expects vars {k=v}), a non-matching run, and edge suppression across a serialized and rebuilt trigger.
Optional KV write: took it. shouldEmit now writes only when matched != previouslyMatched, with the two PowerShell no-rewrite tests ported to EdgeStateTest.
43/43 non-Docker tests pass locally. I still have no Docker, so the container-backed ScriptTriggerEvaluateTest / CommandsTriggerTest need your run or CI. The branch still merges cleanly onto current main.

@Malaydewangan09

Copy link
Copy Markdown
Member

All four addressed in 8521610, plus the write-skip. Ran it with Docker on current main: 55/55, and the ScriptTrigger edge cycle through evaluate() gives FIRED quiet quiet quiet FIRED across exit 1/1/0/0/1. Good from me.

@Malaydewangan09
Malaydewangan09 enabled auto-merge (squash) October 1, 2026 10:02
@Malaydewangan09
Malaydewangan09 merged commit 63de306 into kestra-io:main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Introduce ScriptTrigger & CommandsTrigger for the Lua plugin

3 participants